以 Go + Echo 打造部落格|第 1 篇:專案初始化與骨架(MVP)
日期:2025-10-01(Asia/Taipei)
今天先把地基打好:把專案跑起來、路由打通、目錄整齊、能一鍵啟動。完成後你會有一個最小可行版(MVP)的部落格伺服器:首頁回 HTML、健康檢查回 JSON,之後功能就像樂高慢慢疊上去~😄
小辭典:
Echo:Go 的熱門 Web 框架,用來處理 HTTP 請求/回應。
MVP:Minimum Viable Product,能跑、可用、最小必需。
.env:環境變數檔,把「會因環境不同而改的設定」獨立放。
go mod init
建專案、裝 Echo v4。GET /
:回極簡 HTML(Hello Blog)。GET /health
:回 JSON(健康檢查)。.env
控制埠號,make run
一鍵啟動 🚀我們先把骨架搭起來,再把程式補滿。每一步都能單獨跑,卡住就回來對表。
mkdir go-echo-blog && cd go-echo-blog
go mod init example.com/go-echo-blog
go get github.com/labstack/echo/v4
go get github.com/joho/godotenv
godotenv
:啟動時自動讀.env
,本機開發更輕鬆。
mkdir -p cmd/server
mkdir -p internal/http/handlers
mkdir -p internal/core/domain
mkdir -p internal/core/services
mkdir -p internal/storage/postgres
mkdir -p web/templates
mkdir -p web/static
mkdir -p migrations
先整理一下比較好,這樣等下要放東西才不會打結。
.env
與 Makefile
.env.example
:
cat > .env.example << 'EOF'
APP_ENV=development
PORT=1323
SITE_NAME=My Echo Blog
EOF
複製一份正式檔:
cp .env.example .env
Makefile
:
cat > Makefile << 'EOF'
APP_NAME=go-echo-blog
.PHONY: run dev tidy fmt clean
run:
go run ./cmd/server
dev:
go run ./cmd/server
tidy:
go mod tidy
fmt:
gofmt -w .
clean:
rm -f $(APP_NAME)
EOF
web/templates/index.html
:
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 第 3 篇再接 Tailwind;今天先清爽就好 -->
<style>
body { font-family: system-ui, -apple-system, "Noto Sans TC", Arial; margin: 40px; }
.badge { display:inline-block; padding:4px 8px; border-radius:6px; background:#eef; }
</style>
</head>
<body>
<h1>👋 {{.Title}}</h1>
<p class="badge">MVP ready • {{.Now}}</p>
<p>歡迎來到你的 Go + Echo 部落格!今天先有首頁與健康檢查。</p>
<ul>
<li><a href="/health">/health</a>(JSON 健康檢查)</li>
</ul>
</body>
</html>
internal/http/handlers/home.go
:
package handlers
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
)
// HomeHandler:回首頁 HTML
func HomeHandler(c echo.Context) error {
data := map[string]any{
"Title": "Hello Blog",
"Now": time.Now().In(time.FixedZone("Asia/Taipei", 8*60*60)).Format("2006-01-02 15:04:05"),
}
return c.Render(http.StatusOK, "index.html", data)
}
// HealthHandler:回健康檢查 JSON
func HealthHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]any{
"ok": true,
"app": "go-echo-blog",
"datetime": time.Now().In(time.FixedZone("Asia/Taipei", 8*60*60)).Format(time.RFC3339),
})
}
cmd/server/main.go
:
package main
import (
"html/template"
"io"
"log"
"net/http"
"os"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"example.com/go-echo-blog/internal/http/handlers"
)
// TemplateRenderer 把 html/template 接到 Echo
type TemplateRenderer struct {
t *template.Template
}
func (tr *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error {
return tr.t.ExecuteTemplate(w, name, data)
}
func mustGetEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func main() {
_ = godotenv.Load() // 本機讀 .env;雲端可不需要
port := mustGetEnv("PORT", "1323")
e := echo.New()
e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.Use(middleware.CORS())
// 靜態資源(目前空的)
e.Static("/static", "web/static")
// 模板
t := template.Must(template.ParseGlob("web/templates/*.html"))
e.Renderer = &TemplateRenderer{t: t}
// 路由
e.GET("/", handlers.HomeHandler)
e.GET("/health", handlers.HealthHandler)
e.GET("/_ping", func(c echo.Context) error { return c.String(http.StatusOK, "pong") })
log.Printf("Server on :%s 🚦", port)
if err := e.Start(":" + port); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
go-echo-blog/
├─ cmd/
│ └─ server/
│ └─ main.go
├─ internal/
│ ├─ core/
│ │ ├─ domain/
│ │ └─ services/
│ ├─ http/
│ │ └─ handlers/
│ │ └─ home.go
│ └─ storage/
│ └─ postgres/
├─ migrations/
├─ web/
│ ├─ static/
│ └─ templates/
│ └─ index.html
├─ .env
├─ .env.example
├─ Makefile
└─ go.mod
go.mod
(建立後自動產生,示意):
module example.com/go-echo-blog
go 1.22.0
require (
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.11.4
)
本篇沒有動資料庫(輕鬆開局 ✨)。第 2 篇再用 Docker 起 Postgres、pgxpool
連線、goose
建五張表:users
、posts
、tags
、post_tags
、sessions
。
啟動:
make run
# 看到 "Server on :1323" 就成功
首頁(HTML):
curl -i http://localhost:1323/
健康檢查(JSON):
curl -s http://localhost:1323/health | jq .
存活探針(純文字):
curl -s http://localhost:1323/_ping
# 會回:pong
.env
的 PORT
,或先把舊程式關掉。make run
,而且 web/templates/index.html
路徑別打錯。{{.Title}}
、{{.Now}}
,不要多一個或少一個大括號。.env
沒生效:確認有 cp .env.example .env
,或直接在環境變數設定 PORT
。go.mod
的 module 名要跟 import "example.com/go-echo-blog/..."
對上;用 GitHub 的話請換成自己的 repo 路徑。今天把「骨架」立好了:首頁、健康檢查、環境變數、一鍵啟動都 OK ✅
下一篇(第 2 篇):Docker 啟 Postgres、pgxpool
連線、goose
建表。
小作業(加分):
GET /about
,回一段 HTML,內容放站名與今天日期。2025-10-01 10:30:00
)。為了好維護,提交前可以跑:
make tidy
make fmt